<?php
/* 文字图像
*
*
*/

namespace hpnWse {

require_once(\hpnWse\fGetWseDiry() . 'hpnWse/HttpSvc.php');

use \hpnWse\stNumUtil;
use \hpnWse\stStrUtil;
use \hpnWse\stAryUtil;
use \hpnWse\stObjUtil;
use \hpnWse\stHttpSvc;


/// 分页缓存
class tPgntCch
{
	/// 属性
	public $c_StoDvc; // 存储设备
	public $c_StoDiry; // 目录，末尾不带“/”
	public $c_Trk; // 轨道
	public $c_PageCpct; // 分页容量
	public $c_MaxTot; // 最大总数
	public $c_LifeSpan; // 生命期
	public $c_RcdTot; // 记录总数，获取时若为null则重新计算，否则复用
	public $c_IdxRcd; // 索引记录
	public $c_fFchAllPk; // 获取全部主键
	public $c_fFchRcds; // 获取记录

	private $e_TrkPath;
	private $e_Redis;

	/// 初始化
	/// a_Cfg: Object
	/// {
	/// c_StoDvc: String，存储设备∈{ 'i_FileSys', 'i_Redis' }
	/// c_StoDiry: String，存储目录，必须有效，不要以“/”结尾！
	/// c_Trk：String，轨道，对应a_Cfg[c_StoDiry]里的目录（文件设备）或散列表的键（Redis设备），必须有效
	/// c_PageCpct：Number，分页容量（数据库记录数），默认100，
	///		【警告：请事先考虑好合适的值并坚持使用，一旦改变了这个值，必须清空轨道目录，否则会产生混乱！】
	/// c_LifeSpan: Number，生命期（秒），默认5
	/// c_fFchAllPk: void f(&$a_Pks, $a_PgntCch)，获取全部主键，必须有效
	/// c_fFchRcds: void f(&$a_Rcds, $a_PgntCch, $a_Pks)，获取记录，若要调用cFchRcds则必须有效
	/// }
	public function __construct($a_Cfg)
	{
		$this->c_StoDvc = stObjUtil::cFchPpty($a_Cfg, 'c_StoDvc', 'i_FileSys', array('i_Redis'));
		$this->c_StoDiry = $a_Cfg['c_StoDiry'];
		$this->c_Trk = $a_Cfg['c_Trk'];
		if ($this->cIsStoDvcFile()) // 对于文件，转成路径
		{ $this->e_TrkPath = $this->c_StoDiry . '/' . $this->c_Trk . '.json'; }

		$this->c_PageCpct = stObjUtil::cFchPpty($a_Cfg, 'c_PageCpct', 100);
		$this->c_LifeSpan = stObjUtil::cFchPpty($a_Cfg, 'c_LifeSpan', 5);
		$this->c_fFchAllPk = stObjUtil::cFchPpty($a_Cfg, 'c_fFchAllPk');
		$this->c_fFchRcds = stObjUtil::cFchPpty($a_Cfg, 'c_fFchRcds');

		$this->c_IdxRcd = null;
	}

	/// 计算查询散列值，可用作存储目录名
	/// a_Agms: Object，查询实参，参数值可以是数组，但不能是对象
	/// a_Keys: Array，参与计算的参数名，若对应参数值为null则跳过
	public static function scCalcQryHash($a_Agms, $a_Keys)
	{
		$l_Hash = '';
		foreach ($a_Keys as $l_Idx => $l_An)
		{
			$l_Av = stObjUtil::cFchPpty($a_Agms, $l_An, null);
			if (null === $l_Av)
			{ continue; }

			if (is_array($l_Av))
			{ $l_Hash .= implode(',', $l_Av); }
			else
			{ $l_Hash .= strval($l_Av); }
		}
		return md5($l_Hash);
	}

	/// 缓存作废
	///【说明：如果存储设备是文件，则索引文件变成“{ "c_Tmstp": 0, "c_Tot": -1, "c_Pks": [] }”；
	/// 如果是Redis，则存储目录指向的散列表被删除】
	public static function scCcl($a_StoDvc, $a_StoDiry)
	{
		if ('i_FileSys' === $a_StoDvc)
		{
			$l_Data = '{ "c_Tmstp": 0, "c_Tot": -1, "c_Pks": [] }';
			$l_Iter = new \DirectoryIterator($a_StoDiry);
			$a_StoDiry = stStrUtil::cEnsrDiry($a_StoDiry);
			foreach ($l_Iter as $l_Flnm)
			{
			//	if (preg_match('/[A-Za-z0-9]{32}\.json$/i', $l_Flnm)) // 慢？

				$l_Len = strlen($l_Flnm);
				if ((37 == $l_Len) && ('.json' === substr($l_Flnm, 32)))
				{
					file_put_contents($a_StoDiry . $l_Flnm, $l_Data, LOCK_EX);
				}
			}
		}
		else
		if ('i_Redis' === $a_StoDvc)
		{
			$l_Redis = stHttpSvc::cCnctToDb('redis');
			if ($l_Redis)
			{
				$l_Redis->delete($a_StoDiry);
			}
		}
	}

	/// 存储设备是文件？
	public function cIsStoDvcFile()
	{
		return $this->c_StoDvc === 'i_FileSys';
	}

	/// 存储设备是Redis？
	public function cIsStoDvcRedis()
	{
		return $this->c_StoDvc === 'i_Redis';
	}

	/// 刷新索引
	/// a_Lev: Number，级别，0=读内存、读缓存、重建；1=读缓存、重建，2=重建
	/// 返回：Number，0=缓存未命中，1=命中
	public function cRfshIdx($a_Lev = 0)
	{
		// 用内存的？
		if ((null !== $this->c_IdxRcd) && (0 == $a_Lev))
		{ return 1; }

		// 准备工作
		if ($this->cIsStoDvcFile())
		{ self::ePrprForFile(); }
		else
		{ self::ePrprForRedis(); }

		// 读取索引，按需重建
		$l_ReadRst = null;
		if (2 !== $a_Lev)
		{
			$this->eReadIdxRcd($l_ReadRst);
			if ($l_ReadRst)
			{
				$this->c_IdxRcd = $l_ReadRst;
				if (($l_ReadRst['c_Tot'] >= 0) && // 若作废则为-1
					(time() - $l_ReadRst['c_Tmstp'] < $this->c_LifeSpan)) // 在保质期
				{
					++stHttpSvc::$c_CchHitCnt; // 命中！
					return 1;
				}
			}
		}

		$this->eRcrtIdx();
		return 0;
	}

	/// 修正片段位置和长度，必须先刷新索引
	public function cFixBgnAmt(&$a_Bgn, &$a_Amt)
	{
		$l_Tot = $this->c_IdxRcd['c_Tot'];
		if ($a_Bgn < 0) { $a_Bgn = 0; }
		$l_End = min($l_Tot, $a_Bgn + $a_Amt - 1);
		$a_Amt = $l_End - $a_Bgn + 1;
	}

	/// 获取记录总数
	/// 返回：Number
	public function cFchRcdTot()
	{
		// 刷新索引
		$this->cRfshIdx();
		return $this->c_IdxRcd['c_Tot'];
	}

	/// 获取记录主键片段
	/// a_Rst：Array
	/// a_Bgn: Number, 起始索引≥0
	/// a_Amt: Number，数量＞0
	public function cFchRcdPks(&$a_Rst, $a_Bgn, $a_Amt)
	{
		// 无效范围？
		if (($a_Bgn < 0) || ($a_Amt <= 0))
		{ return array(); }

		// 刷新索引
		$this->cRfshIdx();

		$l_Pks = &$this->c_IdxRcd['c_Pks'];
		$l_End = min($this->c_IdxRcd['c_Tot'] - 1, $a_Bgn + $a_Amt - 1);
		for ($i=$a_Bgn; $i<=$l_End; ++$i)
		{
			$a_Rst[] = $l_Pks[$i];
		}
	}

	/// 获取记录片段
	/// a_Rst：Object[]
	/// a_Bgn: Number, 起始索引≥0
	/// a_Amt: Number，数量＞0
	public function cFchRcds(&$a_Rst, $a_Bgn, $a_Amt)
	{
		// 刷新索引，记录是否命中
		//$l_Hit = $this->cRfshIdx();

		// 获取主键
		$l_Pks = array();
		$this->cFchRcdPks($l_Pks, $a_Bgn, $a_Amt);
		if (!$l_Pks) // 空数组<=>false
		{ return; }

		// // 如果命中，尝试从缓存获取
		// if ($l_Hit)
		// {
		// 	$l_CchData;
		// 	$this->eReadData($l_CchData, $a_Bgn, $a_Amt);
		// }

		$l_Rcds = array();
		$l_CabkAgms = array();
		$l_CabkAgms[] = &$l_Rcds;
		$l_CabkAgms[] = $this;
		$l_CabkAgms[] = &$l_Pks;
		call_user_func_array($this->c_fFchRcds, $l_CabkAgms);

		$l_Len = count($l_Rcds);
		for ($i=0; $i<$l_Len; ++$i)
		{
			$a_Rst[] = $l_Rcds[$i];
		}
	}

	/// 缓存作废（总会强制重建索引）
	public function cCcl()
	{
		self::scCcl($this->c_StoDvc, $this->c_StoDiry);
		$this->c_IdxRcd = null;
		return $this;
	}

	private function ePrprForFile()
	{
		// // 判断轨道目录是否存在，如果不存在，创建之
		// //【注意：mkdir()本身能够保证线程安全（由操作系统负责）】
		// $l_TrkDiry = $this->c_Trk;
		// $l_TrkExi = file_exists($l_TrkDiry);
		// if (!$l_TrkExi)
		// {
		// 	// 这是阻塞调用，即使返回false也不要紧（暗示目录可能刚刚被另一线程创建）
		// 	// 但若还不存在，说明发生比较严重的错误，直接抛出异常
		// 	mkdir($l_TrkDiry);
		// 	if (!file_exists($l_TrkDiry))
		// 	{
		// 		throw new \Exception('无法创建分页缓存轨道目录“' . $l_TrkDiry . '/”！', -1);
		// 	}
		// }
	}

	private function ePrprForRedis()
	{
		// 判断轨道散列表是否存在，如果不存在，创建之
		$this->e_Redis = stHttpSvc::cCnctToDb('redis');
		if (!$this->e_Redis->exists($this->c_StoDiry))
		{
			$this->c_IdxRcd = null;
		}
	}

	// 读取索引记录
	private function eReadIdxRcd(&$a_Rst)
	{
		if ($this->cIsStoDvcFile())
		{
			return $this->eReadIdxRcd_File($a_Rst);
		}
		else
		{
			return $this->eReadIdxRcd_Redis($a_Rst);
		}
	}

	private function eReadIdxRcd_File(&$a_Rst)
	{
		// fstat
		// Array
		// (
		//     [dev] => 771
		//     [ino] => 488704
		//     [mode] => 33188
		//     [nlink] => 1
		//     [uid] => 0
		//     [gid] => 0
		//     [rdev] => 0
		//     [size] => 1114
		//     [atime] => 1061067181(s)
		//     [mtime] => 1056136526(s)
		//     [ctime] => 1056136526(s)
		//     [blksize] => 4096
		//     [blocks] => 8
		// )

		$l_Path = $this->e_TrkPath;
		
		try
		{ $l_FH = file_exists($l_Path) ? fopen($l_Path, 'r') : null; }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_SH)) // 共享锁
		{ return; }

		$l_Stas = fstat($l_FH);
		$a_Rst = fread($l_FH, intval($l_Stas['size']));
		flock($l_FH, LOCK_UN);
		fclose($l_FH);

		if (false !== $a_Rst)
		{ $a_Rst = stObjUtil::cDcdJson($a_Rst); }
	}

	private function eReadIdxRcd_Redis(&$a_Rst)
	{
		// 成功时String，失败时false
		$a_Rst = $this->e_Redis->hGet($this->c_StoDiry, $this->c_Trk);
		if (false === $a_Rst)
		{
			$a_Rst = null;
			return;
		}

	//	stHttpSvc::cLog($this->c_StoDiry . ', ' . $this->c_Trk . ', ' . $l_Rst);
		$a_Rst = stObjUtil::cDcdJson($a_Rst);
	}

	// 重建索引
	private function eRcrtIdx()
	{
		if ($this->cIsStoDvcFile())
		{
			return $this->eRcrtIdx_File();
		}
		else
		{
			return $this->eRcrtIdx_Redis();
		}
	}

	private function eRcrtIdx_File()
	{
		$l_Path = $this->e_TrkPath;
		
		try //【注意这里应该用“c”，参阅文档】
		{ $l_FH = @fopen($l_Path, 'c'); }
		catch (\Exception $a_Exc)
		{ $l_FH = null; }

		if (!$l_FH || !flock($l_FH, LOCK_EX)) // 独占锁
		{ return false; }

		$this->eUpdIdxRcd();

		ftruncate($l_FH, 0);
		$l_Rst = @fwrite($l_FH, stObjUtil::cEcdJson($this->c_IdxRcd));
		fflush($l_FH);

		flock($l_FH, LOCK_UN);
		fclose($l_FH);
		return (false !== $l_Rst);
	}

	private function eRcrtIdx_Redis()
	{
		$this->eUpdIdxRcd();
		$this->e_Redis->hSet($this->c_StoDiry, $this->c_Trk, stObjUtil::cEcdJson($this->c_IdxRcd));
		return true;
	}

	private function eUpdIdxRcd()
	{
		$l_Pks = array();
		$l_CabkAgms = array();
		$l_CabkAgms[] = &$l_Pks;
		$l_CabkAgms[] = $this;
		call_user_func_array($this->c_fFchAllPk, $l_CabkAgms);
		$this->c_IdxRcd = array(
			'c_Tmstp' => time(),
			'c_Tot' => count($l_Pks),
			'c_Pks' => $l_Pks
		);
	}

	// // 读取数据
	// private function eReadData(&$a_Rst, $a_Bgn, $a_Amt)
	// {
	// 	if ($this->cIsStoDvcFile())
	// 	{
	// 		return $this->eReadData_File($a_Rst, $a_Bgn, $a_Amt);
	// 	}
	// 	else
	// 	{
	// 		return $this->eReadData_Redis($a_Rst, $a_Bgn, $a_Amt);
	// 	}
	// }

	// private function eReadData_File(&$a_Rst, $a_Bgn, $a_Amt)
	// {
	// 	$l_Path = $this->c_StoDiry . '/' . $this->c_Trk . '_' . $a_Bgn . '_' . $a_Amt . '.json';
		
	// 	try
	// 	{ $l_FH = file_exists($l_Path) ? fopen($l_Path, 'r') : null; }
	// 	catch (\Exception $a_Exc)
	// 	{ $l_FH = null; }

	// 	if (!$l_FH || !flock($l_FH, LOCK_SH)) // 共享锁
	// 	{ return null; }

	// 	$l_Stas = fstat($l_FH);
	// 	$a_Rst = fread($l_FH, intval($l_Stas['size']));
	// 	flock($l_FH, LOCK_UN);
	// 	fclose($l_FH);

	// 	$a_Rst = stObjUtil::cDcdJson($a_Rst);
	// }

	// // 写入数据记录
	// private function eWrtData()
	// {
	// 	if ($this->cIsStoDvcFile())
	// 	{
	// 		return $this->eRcrtIdx_File();
	// 	}
	// 	else
	// 	{
	// 		return $this->eRcrtIdx_Redis();
	// 	}
	// }

	// private function eRcrtIdx_File()
	// {
	// 	$l_Path = $this->e_TrkPath;
		
	// 	try //【注意这里应该用“c”，参阅文档】
	// 	{ $l_FH = @fopen($l_Path, 'c'); }
	// 	catch (\Exception $a_Exc)
	// 	{ $l_FH = null; }

	// 	if (!$l_FH || !flock($l_FH, LOCK_EX)) // 独占锁
	// 	{ return false; }

	// 	$this->eUpdIdxRcd();

	// 	ftruncate($l_FH, 0);
	// 	$l_Rst = @fwrite($l_FH, stObjUtil::cEcdJson($this->c_IdxRcd));
	// 	fflush($l_FH);

	// 	flock($l_FH, LOCK_UN);
	// 	fclose($l_FH);
	// 	return (false !== $l_Rst);
	// }
}

//【TODO：分两步，先取ID，使用文件指针跳过
// https://www.cnblogs.com/youlechang123/archive/2013/09/15/3322063.html
// pack('L1', 12)
// a - NUL-padded string
// A - SPACE-padded string
// h - Hex string, low nibble first
// H - Hex string, high nibble first
// c - signed char
// C - unsigned char
// s - signed short (always 16 bit, machine byte order)
// S - unsigned short (always 16 bit, machine byte order)
// n - unsigned short (always 16 bit, big endian byte order)
// v - unsigned short (always 16 bit, little endian byte order)
// i - signed integer (machine dependent size and byte order)
// I - unsigned integer (machine dependent size and byte order)
// l - signed long (always 32 bit, machine byte order)
// L - unsigned long (always 32 bit, machine byte order)
// N - unsigned long (always 32 bit, big endian byte order)
// V - unsigned long (always 32 bit, little endian byte order)
// f - float (machine dependent size and representation)
// d - double (machine dependent size and representation)
// x - NUL byte
// X - Back up one byte
// @ - NUL-fill to absolute position
//】

// // 以主键查询无需缓存
// if (($l_QryAgms['c_Id'] > 0) || (count($l_QryAgms['c_IdAry']) > 0))
// {
// 	$l_Rst = fDb_ReadPojs($l_QryAgms);
// 	return array('data' => $l_Rst);
// }
// 】

} // namespace hpnWse

//////////////////////////////////// OVER ////////////////////////////////////